Architecture
Module Map
src/
βββ main.zig application entry point (4 dvui callbacks)
βββ cli.zig CLI subcommands (--cli mode)
βββ PluginIF.zig stable plugin ABI definition
βββ core/
β βββ Schemify.zig netlist generation, hierarchy
β βββ types.zig Wire, Instance, Pin, Net, Prop, Conn
β βββ fileio/
β β βββ Reader.zig CHN parser
β β βββ Writer.zig CHN serializer
β β βββ Toml.zig Config.toml parser
β β βββ utils.zig file I/O helpers
β βββ devices/
β β βββ Devices.zig DeviceKind enum, primitives, PDK models
β βββ simulation/
β βββ SpiceIF.zig simulator interface, Value, SpiceComponent
β βββ Netlist.zig SPICE IR, validation, emission
β βββ backend/ ngspice + Xyce runners
βββ gui/
β βββ lib.zig frame dispatcher (render order)
β βββ Input.zig keyboard/mouse handling
β βββ Actions.zig keybind β Action enum
β βββ Canvas/
β β βββ lib.zig
β β βββ SymbolRenderer.zig
β β βββ WireRenderer.zig
β β βββ Interaction.zig hit-test, drag, rubber-band
β β βββ SelectionOverlay.zig
β β βββ TbOverlay.zig testbench pill button + ghost wires
β βββ Bars/ toolbar, tabbar, command bar
β βββ Panels/ file browser, library, marketplace
β βββ Dialogs/ properties, keybinds, find, spice code
β βββ Keybinds/ keymap definitions
β βββ state/
β βββ AppState.zig global app state (plugins, config, open tabs)
β βββ Document.zig per-tab schematic + selection + tool state
β βββ types.zig
βββ commands/
β βββ lib.zig
β βββ CommandQueue.zig undo/redo ring buffer
β βββ Dispatch.zig Action β handler router
β βββ handlers/ one file per command (AddWire, Move, Deleteβ¦)
β βββ utils/ move, copy, delete helpers
βββ plugins/
β βββ PluginIF.zig message protocol, ABI v6
β βββ Runtime.zig load/tick/unload lifecycle
β βββ Framework.zig helper abstractions for plugin authors
β βββ installer/ plugin install/remove
βββ utility/
β βββ Vfs.zig virtual filesystem (native + WASM)
β βββ Logger.zig structured logging
β βββ Platform.zig OS abstraction
βββ web/ WASM-specific shell (IndexedDB VFS, boot.js)
Data Model
AppState vs Document
AppState (global, process-lifetime):
pub const AppState = struct {
config: Config,
open_tabs: ArrayList(Document),
active_tab: usize,
plugins: PluginHost,
theme: Theme,
};
Document (per-tab, schematic-lifetime):
pub const Document = struct {
schematic: Schematic,
selection: SelectionSet,
tool_state: ToolState, // wire mode, placement mode, etc.
undo_queue: CommandQueue,
file_path: ?[]const u8,
dirty: bool,
};
This split means undo/redo is per-tab, plugins are global.
Schematic Data (DOD)
Schemify uses std.MultiArrayList (Structure-of-Arrays) for all schematic data.
pub const Schematic = struct {
instances: MultiArrayList(Instance),
wires: MultiArrayList(Wire),
nets: MultiArrayList(Net),
labels: MultiArrayList(Text),
};
// Fast: iterating all X coords is contiguous memory
const xs = schematic.instances.items(.x);
const ys = schematic.instances.items(.y);
Core Types
pub const Wire = struct {
x0, y0, x1, y1: i32,
net_name: ?[]const u8,
bus: bool,
};
pub const Instance = struct {
name: []const u8, // "R1", "M2", "Xamp"
symbol: []const u8, // reference to .chn_prim or .chn
x, y: i32,
rot: u2, // 0=0Β°, 1=90Β°, 2=180Β°, 3=270Β°
flip: bool,
kind: DeviceKind,
prop_start: u32, // index into flat property array
prop_count: u16,
conn_start: u32, // index into flat connection array
conn_count: u16,
};
pub const Prop = struct { key, val: []const u8 };
pub const Conn = struct { pin, net: []const u8 };
Net connectivity uses union-find (NetMap): wires are merged into nets by scanning endpoints.
Command Queue and Undo
All mutations go through the Command union in commands/:
pub const UndoableAction = union(enum) {
add_wire: WireAddCmd,
delete_selection: DeleteCmd,
move_instances: MoveCmd,
copy_paste: PasteCmd,
rotate_cw, rotate_ccw,
flip_h, flip_v,
set_instance_prop: PropCmd,
// ...
};
CommandQueue is a ring buffer β push appends, undo pops and inverts, redo re-applies.
// Trigger from GUI
actions.enqueue(app, .{ .undoable = .{ .add_wire = .{
.start = p0,
.end = p1,
} } }, "Add wire");
Handlers in commands/handlers/ implement handle() and undo() for each action.
VFS β Virtual Filesystem
All file I/O goes through utility/Vfs.zig. Calling std.fs directly outside utility/ and cli/ is banned (enforced by lint).
Why: WASM has no native filesystem. VFS maps to:
- Native:
std.fs - Web: IndexedDB via JS interop
// In plugin or core code:
const data = try Vfs.readAlloc(allocator, "config.toml");
defer allocator.free(data);
try Vfs.writeAll("output.spice", netlist_text);
try Vfs.makePath("cache/pdk");
Rendering Pipeline
Frame order (back β front):
- Grid
- Wires (
WireRenderer) - Instance bodies (
SymbolRenderer) - Instance pins
- Net labels
- Selection highlights (
SelectionOverlay) - Rubber-band rectangle
- Testbench ghost overlay (
TbOverlay) β only when hovering TB pill - Plugin overlays (floating panels)
- UI chrome (topbar, sidebar, toolbar, statusbar)
Testbench Overlay
When a .chn schematic is open and a matching .chn_tb exists in the same project:
- Pill button appears in top-right of canvas:
βΆ test.chn_tb - Hover β ghost-draws testbench wires on top of the DUT schematic (shows port connections)
- Click β switch active tab to the testbench
- Shift+Click β open testbench in a new tab
Implemented in gui/Canvas/TbOverlay.zig.
Dual Backend
zig build # native (SDL3 + Raylib, OpenGL)
zig build -Dbackend=web # WASM (HTML5 Canvas via dvui wasm32 backend)
Same source. Platform-specific code isolated to web/ and the dvui backend selection in build.zig. VFS, plugin runtime, and all core logic are backend-agnostic.
CLI Mode
Schemify has a headless CLI mode β no display, no dvui:
zig build run -- --cli help
zig build run -- --cli netlist output.spice schematic.chn
zig build run -- --cli export-svg render.svg schematic.chn
zig build run -- --cli plugin-install ./libMyPlugin.so
Implemented in src/cli.zig. CLI mode compiles out the GUI entirely β no Xvfb needed in CI.